﻿/* 
 * Enterprise Library Extensions - DI + ORM.
 * Copyright (C) 2006
 *
 * GNU General Public License
 *
 * This program is free software; you can redistribute 
 * it and/or modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation; either version 2 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 * See the GNU General Public License for more details.
 */
#region Using directives

using System;
using System.Collections;
using System.Text;
using System.Reflection;
using ELE.Common.Util;
using ELE.EnterpriseLibrary.Naming.Spi;
using ELE.EnterpriseLibrary.Naming.Configuration;
using ELE.EnterpriseLibrary.Common.Logging;

#endregion

namespace ELE.EnterpriseLibrary.Naming.Factories
{
  /// <summary>
  /// Implementation specific
  /// </summary>
	public class DefaultNamingObjectFactory : IObjectFactory
	{
    /// <summary>
    /// Default constructor
    /// </summary>
		public DefaultNamingObjectFactory()
		{			
		}
    /// <summary>
    /// Parses configured values to an actual object
    /// </summary>
    /// <param name="initializer"></param>
    /// <param name="ctx"></param>
    /// <param name="environment"></param>
    /// <returns></returns>
    protected object GetObjectFromGenericCollection(GenericCollectionInitializer initializer, INamingContext ctx, Hashtable environment)
    {
      if (initializer != null)
      {
        string genericType = (string)GetStringDefinedObject(ctx, initializer.GenericType, typeof(string));
        string genericValueType = (string)GetStringDefinedObject(ctx, initializer.GenericValueType, typeof(string));
        if (initializer.Type == 0)
        {
          //Entries are list
          object o = ObjectFactory.CreateGenericList( genericType, genericValueType);
          foreach (object entry in initializer.Entries)
          {
            object val = GetObjectFromValue(entry, genericValueType, ctx, environment);
            MethodInfo method = o.GetType().GetMethod("Add");
            object[] pars = { val };
            method.Invoke(o, pars);
          }
          return o;
        }
        else if (initializer.Type == 1)
        {
          //Entries are dictionaries
          string genericKeyType = (string)GetStringDefinedObject(ctx, initializer.GenericKeyType, typeof(string));
          object o = ObjectFactory.CreateGenericDictionary( genericType, genericKeyType, genericValueType );
          foreach (KeyValuePair entry in initializer.Entries)
          {
            object key = GetObjectFromValue(entry.Key, genericKeyType, ctx, environment);
            object val = GetObjectFromValue(entry.Value, genericValueType, ctx, environment);
            MethodInfo method = o.GetType().GetMethod("Add");
            object[] pars = { key, val };
            method.Invoke(o, pars);
          }
          return o;
        }
      }
      return null;
    }
    private object GetObjectFromValue(object val, string stringValueType, INamingContext ctx, Hashtable environment)
    {
      if (val is ComponentInitializer)
      {
        return GetObjectFromInitializer((ComponentInitializer)val, ctx, environment);
      }
      else if (val is string)
      {
        return this.GetStringDefinedObject(ctx, (string)val, Type.GetType(stringValueType));
      }
      else
      {
        return ResolveObjectByType(val, ctx, environment);
      }
    }
    /// <summary>
    /// Extracts objects and references from given hashtable and returns parsed values in a hashtable
    /// </summary>
    /// <param name="args"></param>
    /// <param name="ctx"></param>
    /// <param name="environment"></param>
    /// <returns></returns>
    protected object GetObjectFromHashtable(IDictionary args, INamingContext ctx, Hashtable environment)
    {
      Hashtable vals = new Hashtable();
      foreach (string key in args.Keys)
      {
        if (args[key] is ComponentInitializer)
        {
          try
          {
            vals.Add(key, GetObjectFromInitializer((ComponentInitializer)args[key], ctx, environment));
          }
          catch(CircularReferenceException cre)
          {
            ctx.Log("Circular reference exception caught, cannot resolve circular dependencies on hashtables. Use a property instead if necessary. Ignoring hastable entry", cre, LogLevel.Warning);
          }
        }
        else if(args[key] is string)
        {
          vals.Add(key, args[key]);
        }
        else
        {
          vals.Add(key, ResolveObjectByType(args[key], ctx, environment));
        }
      }
      return vals;
    }

    private object ResolveObjectByType(object obj, INamingContext ctx, Hashtable environment)
    {
      if(obj is IDictionary)
      {
        return GetObjectFromHashtable((IDictionary)obj, ctx, environment);
      }
      else if(obj is IList)
      {
        return GetObjectFromList((IList)obj, ctx, environment);
      }
      else if (obj is GenericCollectionInitializer)
      {
        return GetObjectFromGenericCollection((GenericCollectionInitializer)obj, ctx, environment);
      }
      return obj;
    }
    /// <summary>
    /// Extracts objects and references from given list and returns parsed values in a list
    /// </summary>
    /// <param name="args"></param>
    /// <param name="ctx"></param>
    /// <param name="environment"></param>
    /// <returns></returns>
    protected object GetObjectFromList(IList args, INamingContext ctx, Hashtable environment)
    {
      ArrayList vals = new ArrayList();
      foreach (object val in args)
      {
        if (val is ComponentInitializer)
        {
          try
          {
            vals.Add(GetObjectFromInitializer((ComponentInitializer)val, ctx, environment));
          }
          catch(CircularReferenceException cre)
          {
            ctx.Log("Circular reference exception caught, cannot resolve circular dependencies on lists. Use a property instead if necessary. Ignoring list entry", cre, LogLevel.Warning);
          }
        }
        else if (val is string)
        {
          vals.Add(val);
        }
        else
        {
          vals.Add(ResolveObjectByType(val, ctx, environment));
        }
      }
      return vals;
    }
    /// <summary>
    /// Builds an object based on a definition from a component initializer objet.
    /// If the initializer points to a reference, this method will do a lookup in the context (ctx) 
    /// and if an object is not found in the context it will call GetObjectInstance to create one.
    /// </summary>
    /// <param name="args"></param>
    /// <param name="ctx"></param>
    /// <param name="environment"></param>
    /// <returns></returns>
    public object GetObjectFromInitializer(ComponentInitializer args, INamingContext ctx, Hashtable environment)
    {
      if (args.Type == ComponentInitializer.TYPE_REF)
      {
        return ComponentObjectFactory.GetReferencedObjectFromComponentInitializer(args, ctx);
      }
      else if (args.Value is IDictionary )
      {
        return GetObjectFromHashtable((IDictionary)args.Value, ctx, environment);
      }
      else if (args.Value is IList)
      {
        return GetObjectFromList((IList)args.Value, ctx, environment);
      }
      else if (args.Value is string)
      {
        return args.Value;
      }
      else if (args.Value is GenericCollectionInitializer)
      {
        return GetObjectFromGenericCollection((GenericCollectionInitializer)args.Value, ctx, environment);
      }
      return null;
    }
    #region IObjectFactory Members
    /// <summary>
    /// Implementation of the object factory getobjectinstance method that creates and binds an object by its name
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="name"></param>
    /// <param name="nameCtx"></param>
    /// <param name="environment"></param>
    /// <returns></returns>
    public object GetObjectInstance(object obj, IName name, INamingContext nameCtx, Hashtable environment)
    {
      object objInst = null;
      INamingContext ctx = null;

      if (nameCtx != null)
      {
        ctx = nameCtx;
      }
      else
      {
        ctx = NamingManager.GetInitialContext(environment);
      }
      if (environment[ELE.EnterpriseLibrary.Naming.NamingContextConstants.LOCAL_CONTEXT_ROOT] != null)
      {
        //TODO: This now only deals with objects in the context of the file system and Composite names
        // must be extended to use remoting and other context types.
        string ctxRoot = (string)environment[ELE.EnterpriseLibrary.Naming.NamingContextConstants.LOCAL_CONTEXT_ROOT];
        if (!ctxRoot.EndsWith(StringUtils.SLASH_FORWARD))
        {
          ctxRoot += StringUtils.SLASH_FORWARD;
        }
        string objConfigLocation = FileUtil.VerifySeparator( name.ToString());
        if (objConfigLocation.StartsWith(StringUtils.SLASH_FORWARD))
        {
          objConfigLocation = objConfigLocation.Substring(1);
        }
        objConfigLocation = ctxRoot + objConfigLocation + NamingContextConstants.CONFIG_EXTENSION;
        if (System.IO.File.Exists(objConfigLocation))
        {
          try
          {
            ComponentConfiguration conf = ComponentConfigurationParser.ParseComponentConfiguration(objConfigLocation);
            if (conf != null && conf.ComponentType != null)
            {
              if(StringUtils.Contains(conf.ComponentType.AssemblyName, @"${"))
              {
                conf.ComponentType.AssemblyName = (string)GetStringDefinedObject(ctx, conf.ComponentType.AssemblyName, Type.GetType("System.String"));
              }
              ConstructorInfo constructor = null;
              MethodInfo factoryMethod = null;
              //TODO: Factory methods.
              if (conf.Factory != null)
              {
                factoryMethod = ComponentObjectFactory.GetProperFactory(conf);
              }
              else
              {
                constructor = ComponentObjectFactory.GetProperConstructor(conf);
              }
              if (constructor != null || factoryMethod != null)
              {
                ParameterInfo[] parameters = null;
                ComponentInitializerCollection arguments = null;
                object[] inputs = null;
                if (constructor != null)
                {
                  parameters = constructor.GetParameters();
                  arguments = conf.ConstructorArguments;
                }
                else
                {
                  parameters = factoryMethod.GetParameters();
                  arguments = conf.Factory.FactoryMethodParameters;
                }
                inputs = new object[parameters.Length];

                int paramIndex = 0;

                foreach (ParameterInfo param in parameters)
                {
                  foreach (ComponentInitializer args in arguments)
                  {
                    if (param.Name.Equals(args.Name))
                    {
                      object val = null;
                      try
                      {
                        val = GetObjectFromInitializer(args, ctx, environment);
                      }
                      catch(ResourceInitializationException rie)
                      {
                        ctx.Log(String.Format("DefaultNamingObjectFactory ; Resource Initialization Exception caught when setting constructor argument named {0} to resource named {1}, setting argument value to null.",args.Name,name.ToString()), rie, LogLevel.Warning);
                        val = null;
                      }
                      catch(CircularReferenceException cre)
                      {
                        ctx.Log("Circular reference exception caught, cannot resolve circular dependencies on constructors. Use a property instead if necessary. Setting value to null.", cre, LogLevel.Warning);
                      }
                      if (val != null)
                      {
                        if (val is string)
                        {
                          inputs[paramIndex] = GetStringDefinedObject(ctx, (string)val, param.ParameterType);
                        }
                        else if (param.ParameterType.IsAssignableFrom(val.GetType()))
                        {
                          inputs[paramIndex] = val;
                        }
                        else
                        {
                          inputs[paramIndex] = null;
                        }
                      }
                      else
                      {
                        inputs[paramIndex] = null;
                      }
                      break;
                    }
                  }
                  paramIndex += 1;
                }

                try
                {
                  if (constructor != null)
                  {
                    objInst = constructor.Invoke(inputs);
                  }
                  else if (factoryMethod != null)
                  {
                    objInst = factoryMethod.Invoke(null, inputs);
                  }
                  ctx.Log("ObjectFactory: Created instance of object named " + name.ToString() + " in context of type " + ctx.GetType().FullName, 
                    null, 
                    LogLevel.Debug);
                  SetObjectProperties(conf, objInst, ctx, environment, name);
                  RunObjectMethods(conf, objInst, ctx, environment);
                }
                catch (Exception cEx)
                {
                  //Logging
                  ctx.Log("ObjectFactory: Error creating instance or initializing object named " + name.ToString() + " using class name " + conf.ComponentType.ClassAssemblyName, cEx.InnerException, LogLevel.Error);
                  throw new ResourceInitializationException(cEx.Message, cEx);
                }

              }
              else
              {
                ctx.Log("ObjectFactory: Error creating instance of object named " + name.ToString() + " No constructor found for class name " + conf.ComponentType.ClassAssemblyName, null, LogLevel.Error);
                throw new ResourceInitializationException("No class or constructor found for object named " + name.ToString() + " in the context. The configured class type is " + conf.ComponentType.ClassAssemblyName + ".");
              }
            }
            else
            {
              throw new ResourceInitializationException(String.Format("Configuration file for resource named {0} at location {1} could not be parsed or does not have a type definition",name.ToString(),objConfigLocation) );
            }
          }
          catch (Exception exep)
          {
            if(!(exep is ResourceInitializationException))
            {
              ctx.Log("ObjectFactory: Unknown Error creating instance of object named " + name.ToString() + " The exception is ", exep, LogLevel.Error);
            }
            throw exep;
          }
        }
        else
        {
          ctx.Log("ObjectFactory: Configuration file not found for component named " + name.ToString() + ", The location is " + objConfigLocation, null, LogLevel.Warning);
        }

      }
//      if (objInst != null)
//      {
//        if (ctx != null)
//        {
//          ctx.Bind(name, objInst);
//        }
//      }
      return objInst;
    }

    #endregion

    /// <summary>
    /// Sets the property values from the defined properties
    /// </summary>
    /// <param name="conf">The parsed component configuration</param>
    /// <param name="obj">the object to mutate</param>
    /// <param name="ctx">the context to get references from</param>
    /// <param name="environment">the environment to use</param>
    /// <param name="name">The name of the object we are resolving</param>
    protected void SetObjectProperties(ComponentConfiguration conf, object obj, INamingContext ctx, Hashtable environment, IName name)
    {
      if (conf == null || obj == null || conf.Properties == null)
      {
        return;
      }
      foreach (ComponentInitializer args in conf.Properties)
      {
        PropertyInfo property = obj.GetType().GetProperty(args.Name);
        if ( property != null && property.CanWrite)
        {
          object val = null;
          try
          {
            val = GetObjectFromInitializer(args, ctx, environment);
          }
          catch(ResourceInitializationException rie)
          {
            ctx.Log(String.Format("DefaultNamingObjectFactory ; Resource Initialization Exception caught when setting property named {0} to resource named {1}, setting property value to null.",args.Name,name.ToString()), rie, LogLevel.Warning);
            val = null;
            if(ctx is InitialContext)
            {
              string componentName =  GenericConfiguration.ESCAPE_INDEX_START + 
                name.ToString() + 
                GenericConfiguration.ESCAPE_INDEX_END +
                args.Name;
              string referenceName = ComponentObjectFactory.GetComponentNameFromReference((string)args.Value);
              ((InitialContext)ctx).RemoveUnresolvedReferencesForNamedObject(referenceName);
              ctx.Log(String.Format("Removing any circular references left to resolve for invalid object {0}, due to ResourceInitializationException while setting property {1}", referenceName, componentName), null, LogLevel.Debug);
            }
          }
          catch(CircularReferenceException cre)
          {
            if(ctx is InitialContext)
            {
              string componentName =  GenericConfiguration.ESCAPE_INDEX_START + 
                name.ToString() + 
                GenericConfiguration.ESCAPE_INDEX_END +
                args.Name;
              string referenceName = (string)args.Value;
              ((InitialContext)ctx).AddUnresolvedReference(componentName, referenceName);
              ctx.Log(String.Format("Circular reference exception caught, adding to unresolved list {0} = {1}", componentName, referenceName), null, LogLevel.Debug);
            }
            else
            {
              ctx.Log("Circular reference exception caught. Cannot resolve circular dependencies in non initial context types.", cre, LogLevel.Warning);
            }
          }
          if (val != null)
          {
            if (val is string)
            {
              try
              {
                property.SetValue(obj, 
                                  GetStringDefinedObject(ctx, (string)val, property.PropertyType), 
                                  null);
              }
              catch (Exception e)
              {
                ctx.Log("Could not set value " + val + 
                                   " to property type " + property.Name + 
                                   " of object " + obj.GetType().FullName, 
                                   e, 
                                   LogLevel.Error);
              }
            }
            else if (property.PropertyType.IsAssignableFrom(val.GetType()))
            {
              property.SetValue(obj, val, null);
            }
          }
        }
      }
    }

    /// <summary>
    /// Runs the methods defined in the configuration
    /// </summary>
    /// <param name="conf">The parsed component configuration</param>
    /// <param name="obj">the object to mutate</param>
    /// <param name="ctx">the context to get references from</param>
    /// <param name="environment">the environment to use</param>
    protected void RunObjectMethods(ComponentConfiguration conf, object obj, INamingContext ctx, Hashtable environment)
    {
      if (conf == null || obj == null || conf.Methods == null)
      {
        return;
      }
      foreach (ComponentInitializer args in conf.Methods)
      {
        RunObjectMethod(args, obj, ctx, environment);
      }
    }
    /// <summary>
    /// Runs a particular method and returns the object from its return value
    /// </summary>
    /// <param name="methodDef"></param>
    /// <param name="obj"></param>
    /// <param name="ctx"></param>
    /// <param name="environment"></param>
    /// <returns></returns>
    protected object RunObjectMethod(ComponentInitializer methodDef, object obj, INamingContext ctx, Hashtable environment)
    {
      if (methodDef != null && obj != null)
      {
        MethodInfo[] methods = obj.GetType().GetMethods();
        if (methods != null)
        {
          Hashtable inputs = new Hashtable();
          if (methodDef.Value != null)
          {
            foreach (ComponentInitializer paramDef in (ComponentInitializerCollection)methodDef.Value)
            {
              try
              {
                inputs.Add(paramDef.Name, GetObjectFromInitializer(paramDef, ctx, environment));
              }
              catch(ResourceInitializationException rie)
              {
                ctx.Log(String.Format("DefaultNamingObjectFactory ; Resource Initialization Exception caught when running method named {0} adn input named {1} to object of type {2}, setting method input value to null.",methodDef.Name,paramDef.Name,obj.GetType().FullName), rie, LogLevel.Warning);
                inputs.Add(paramDef.Name, null);
              }
              catch(CircularReferenceException cre)
              {
                ctx.Log("Circular reference exception caught, cannot resolve circular dependencies on methods. Use a property instead if necessary. Assigning null value", cre, LogLevel.Warning);
                inputs.Add(paramDef.Name, null);
              }
            }
          }
          foreach (MethodInfo method in methods)
          {
            if (method.Name.Equals(methodDef.Name) && method.GetParameters().Length == inputs.Count)
            {
              bool allMatch = true;
              object[] inParams = new object[method.GetParameters().Length];
              int pIndex = 0;
              foreach (ParameterInfo param in method.GetParameters())
              {
                if (inputs[param.Name] == null)
                {
                  inParams[pIndex] = null;
                }
                else if (inputs[param.Name] is string)
                {
                  try
                  {
                    object val = Convert.ChangeType(inputs[param.Name], param.ParameterType);
                    inParams[pIndex] = val;
                  }
                  catch 
                  {
                    allMatch = false;
                    break;
                  }
                }
                else if (param.ParameterType.IsAssignableFrom(inputs[param.Name].GetType()))
                {
                  inParams[pIndex] = inputs[param.Name];
                }
                else
                {
                  allMatch = false;
                  break;
                }
                pIndex++;
              }
              if (allMatch)
              {
                return method.Invoke(obj, inParams);
              }
            }
          }
        }
      }
      return null;
    }
    /// <summary>
    /// Parses out a string that may contain links to context variables
    /// </summary>
    /// <param name="ctx"></param>
    /// <param name="value"></param>
    /// <param name="targetType"></param>
    /// <returns></returns>
    protected object GetStringDefinedObject(INamingContext ctx, string value, Type targetType)
    {
      return ContextValueParser.GetStringDefinedObject(ctx.Environment, ctx, value, targetType);
    }
  }
}
